/*
 * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

/*
 * @test
 * @summary Testing Signatures.
 * @bug 8321540 8319463 8357955 8368050 8367585 8368331
 * @run junit SignaturesTest
 */
import java.io.IOException;
import java.lang.classfile.attribute.SignatureAttribute;
import java.lang.constant.ClassDesc;
import java.net.URI;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
import java.lang.classfile.ClassSignature;
import java.lang.classfile.ClassFile;
import java.lang.classfile.MethodSignature;
import java.lang.classfile.Signature;
import java.lang.classfile.Signature.*;
import java.lang.classfile.Attributes;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

import static helpers.ClassRecord.assertEqualsDeep;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static java.lang.constant.ConstantDescs.*;

class SignaturesTest {

    private static final FileSystem JRT = FileSystems.getFileSystem(URI.create("jrt:/"));

    @Test
    void testBuildingSignatures() {
        assertEqualsDeep(
                ClassSignature.of(
                        ClassTypeSig.of(
                                ClassTypeSig.of(ClassDesc.of("java.util.LinkedHashMap"), TypeArg.of(TypeVarSig.of("K")), TypeArg.of(TypeVarSig.of("V"))),
                                "LinkedHashIterator"),
                        ClassTypeSig.of(ClassDesc.of("java.util.Iterator"),
                                TypeArg.of(ClassTypeSig.of(ClassDesc.of("java.util.Map$Entry"), TypeArg.of(TypeVarSig.of("K")), TypeArg.of(TypeVarSig.of("V")))))),
                ClassSignature.parseFrom("Ljava/util/LinkedHashMap<TK;TV;>.LinkedHashIterator;Ljava/util/Iterator<Ljava/util/Map$Entry<TK;TV;>;>;"));

        assertEqualsDeep(
                ClassSignature.of(
                        List.of(
                                TypeParam.of("K", ClassTypeSig.of(CD_Object)),
                                TypeParam.of("V", ClassTypeSig.of(CD_Object))),
                        ClassTypeSig.of(ClassDesc.of("java.util.AbstractMap"), TypeArg.of(TypeVarSig.of("K")), TypeArg.of(TypeVarSig.of("V"))),
                        ClassTypeSig.of(ClassDesc.of("java.util.concurrent.ConcurrentMap"), TypeArg.of(TypeVarSig.of("K")), TypeArg.of(TypeVarSig.of("V"))),
                        ClassTypeSig.of(ClassDesc.of("java.io.Serializable"))),
                ClassSignature.parseFrom("<K:Ljava/lang/Object;V:Ljava/lang/Object;>Ljava/util/AbstractMap<TK;TV;>;Ljava/util/concurrent/ConcurrentMap<TK;TV;>;Ljava/io/Serializable;"));

        assertEqualsDeep(
                MethodSignature.of(
                        ClassTypeSig.of(
                                CD_Map,
                                TypeArg.of(ClassTypeSig.of(
                                        CD_Class,
                                            TypeArg.extendsOf(
                                                    ClassTypeSig.of(ClassDesc.of("java.lang.annotation.Annotation"))))),
                                TypeArg.of(ClassTypeSig.of(ClassDesc.of("java.lang.annotation.Annotation")))),
                        Signature.of(CD_byte.arrayType()),
                        ClassTypeSig.of(ClassDesc.of("jdk.internal.reflect.ConstantPool")),
                        ClassTypeSig.of(CD_Class, TypeArg.unbounded()),
                        ArrayTypeSig.of(
                                ClassTypeSig.of(CD_Class,
                                        TypeArg.extendsOf(
                                                ClassTypeSig.of(ClassDesc.of("java.lang.annotation.Annotation")))))),
                MethodSignature.parseFrom("([BLjdk/internal/reflect/ConstantPool;Ljava/lang/Class<*>;[Ljava/lang/Class<+Ljava/lang/annotation/Annotation;>;)Ljava/util/Map<Ljava/lang/Class<+Ljava/lang/annotation/Annotation;>;Ljava/lang/annotation/Annotation;>;"));

        assertEqualsDeep(
                MethodSignature.of(
                        List.of(
                                TypeParam.of("T", Optional.empty(), ClassTypeSig.of(ClassDesc.of("java.lang.annotation.Annotation")))),
                        List.of(
                                ClassTypeSig.of(ClassDesc.of("java.lang.IOException")),
                                ClassTypeSig.of(ClassDesc.of("java.lang.IllegalAccessError"))),
                        ArrayTypeSig.of(TypeVarSig.of("T")),
                        ClassTypeSig.of(CD_Class, TypeArg.of(TypeVarSig.of("T")))),
                MethodSignature.parseFrom("<T::Ljava/lang/annotation/Annotation;>(Ljava/lang/Class<TT;>;)[TT;^Ljava/lang/IOException;^Ljava/lang/IllegalAccessError;"));

        assertEqualsDeep(
                ClassTypeSig.of(
                        CD_Set,
                        TypeArg.extendsOf(
                                ClassTypeSig.of(ClassDesc.of("java.nio.file.WatchEvent$Kind"), TypeArg.unbounded()))),
                Signature.parseFrom("Ljava/util/Set<+Ljava/nio/file/WatchEvent$Kind<*>;>;"));

        assertEqualsDeep(
                ArrayTypeSig.of(2, TypeVarSig.of("E")),
                Signature.parseFrom("[[TE;"));

        assertEqualsDeep(
                MethodSignature.of(
                        List.of(TypeParam.of("A", ClassTypeSig.of("one/Two")), // /
                                TypeParam.of("B", ClassTypeSig.of(ClassTypeSig.of("Outer"), "Inner")), // .
                                TypeParam.of("C", ArrayTypeSig.of(BaseTypeSig.of('I'))), // [
                                TypeParam.of("D", ClassTypeSig.of("Generic", TypeArg.unbounded())), // <
                                TypeParam.of("E", TypeVarSig.of("A")), // ;
                                TypeParam.of("F", (ClassTypeSig) null), // :
                                TypeParam.of("G", (ClassTypeSig) null)), // >
                        List.of(),
                        BaseTypeSig.of('V')),
                MethodSignature.parseFrom("<A:Lone/Two;B:LOuter.Inner;C:[ID:LGeneric<*>;E:TA;F:G:>()V")
        );
    }

    @Test
    void testGenericCreationChecks() {
        var weirdNameClass = ClassDesc.of("<Unsupported>");
        var voidSig = BaseTypeSig.of('V');
        Assertions.assertThrows(IllegalArgumentException.class, () -> Signature.of(weirdNameClass));
        Assertions.assertThrows(IllegalArgumentException.class, () -> BaseTypeSig.of(CD_Object));
        Assertions.assertThrows(IllegalArgumentException.class, () -> BaseTypeSig.of(CD_int.arrayType()));
        Assertions.assertThrows(IllegalArgumentException.class, () -> ClassTypeSig.of(CD_int));
        Assertions.assertThrows(IllegalArgumentException.class, () -> ClassTypeSig.of(CD_Object.arrayType()));
        Assertions.assertThrows(IllegalArgumentException.class, () -> ClassTypeSig.of(weirdNameClass));
        Assertions.assertThrows(IllegalArgumentException.class, () -> ArrayTypeSig.of(voidSig));
        Assertions.assertThrows(IllegalArgumentException.class, () -> ArrayTypeSig.of(255, voidSig));
        Assertions.assertThrows(IllegalArgumentException.class, () -> MethodSignature.of(voidSig, voidSig));
        Assertions.assertThrows(IllegalArgumentException.class, () -> MethodSignature.of(List.of(), List.of(), voidSig, voidSig));
    }

    static Stream<String> goodIdentifiers() {
        return Stream.of("T", "Hello", "Mock", "(Weird)", " Huh? ");
    }

    static Stream<String> badIdentifiers() {
        return Stream.of("", ";", ".", "/", "<", ">", "[", ":",
                "<Unsupported>", "has.chars", "/Outer", "test/", "test//Outer");
    }

    static Stream<String> slashedIdentifiers() {
        return Stream.of("test/Outer", "java/lang/Integer");
    }

    @ParameterizedTest
    @MethodSource({"badIdentifiers", "slashedIdentifiers"})
    void testBadSimpleIdentifier(String st) {
        ClassTypeSig outer = ClassTypeSig.of("test/Outer");
        Assertions.assertThrows(IllegalArgumentException.class, () -> ClassTypeSig.of(outer, st));
        Assertions.assertThrows(IllegalArgumentException.class, () -> TypeVarSig.of(st));
        Assertions.assertThrows(IllegalArgumentException.class, () -> TypeParam.of(st, (RefTypeSig) null));
        Assertions.assertThrows(IllegalArgumentException.class, () -> TypeParam.of(st, Optional.empty()));
    }

    @ParameterizedTest
    @MethodSource("goodIdentifiers")
    void testGoodSimpleIdentifier(String st) {
        ClassTypeSig outer = ClassTypeSig.of("test/Outer");
        ClassTypeSig.of(outer, st);
        TypeVarSig.of(st);
        TypeParam.of(st, (RefTypeSig) null);
        TypeParam.of(st, Optional.empty());
    }

    @ParameterizedTest
    @MethodSource("badIdentifiers")
    void testBadSlashedIdentifier(String st) {
        Assertions.assertThrows(IllegalArgumentException.class, () -> ClassTypeSig.of(st));
    }

    @ParameterizedTest
    @MethodSource({"goodIdentifiers", "slashedIdentifiers"})
    void testGoodSlashedIdentifier(String st) {
        ClassTypeSig.of(st);
    }

    @Test
    void testParseAndPrintSignatures() throws Exception {
        var csc = new AtomicInteger();
        var msc = new AtomicInteger();
        var fsc = new AtomicInteger();
        var rsc = new AtomicInteger();
        Stream.of(
                Files.walk(JRT.getPath("modules/java.base")),
                Files.walk(JRT.getPath("modules"), 2).filter(p -> p.endsWith("module-info.class")),
                Files.walk(Path.of(SignaturesTest.class.getProtectionDomain().getCodeSource().getLocation().toURI())))
                .flatMap(p -> p)
                .filter(p -> Files.isRegularFile(p) && p.toString().endsWith(".class")).forEach(path -> {
            try {
                var cm = ClassFile.of().parse(path);
                cm.findAttribute(Attributes.signature()).ifPresent(csig -> {
                    assertEquals(
                            ClassSignature.parseFrom(csig.signature().stringValue()).signatureString(),
                            csig.signature().stringValue(),
                            cm.thisClass().asInternalName());
                    csc.incrementAndGet();
                });
                for (var m : cm.methods()) {
                    m.findAttribute(Attributes.signature()).ifPresent(msig -> {
                        assertEquals(
                                MethodSignature.parseFrom(msig.signature().stringValue()).signatureString(),
                                msig.signature().stringValue(),
                                cm.thisClass().asInternalName() + "::" + m.methodName().stringValue() + m.methodType().stringValue());
                        msc.incrementAndGet();
                    });
                }
                for (var f : cm.fields()) {
                    f.findAttribute(Attributes.signature()).ifPresent(fsig -> {
                        assertEquals(
                                Signature.parseFrom(fsig.signature().stringValue()).signatureString(),
                                fsig.signature().stringValue(),
                                cm.thisClass().asInternalName() + "." + f.fieldName().stringValue());
                        fsc.incrementAndGet();
                    });
                }
                cm.findAttribute(Attributes.record()).ifPresent(reca
                        -> reca.components().forEach(rc -> rc.findAttribute(Attributes.signature()).ifPresent(rsig -> {
                    assertEquals(
                            Signature.parseFrom(rsig.signature().stringValue()).signatureString(),
                            rsig.signature().stringValue(),
                            cm.thisClass().asInternalName() + "." + rc.name().stringValue());
                    rsc.incrementAndGet();
                })));
            } catch (Exception e) {
                throw new AssertionError(path.toString(), e);
            }
        });
        System.out.println("SignaturesTest - tested signatures of " + csc + " classes, " + msc + " methods, " + fsc + " fields and " + rsc + " record components");
    }

    static class Outer<T1> {
        class Inner<T2> {}
    }

    static class Observer extends ArrayList<Outer<String>.Inner<Long>>{}

    @Test
    void testClassSignatureClassDesc() throws IOException {
        var observerCf = ClassFile.of().parse(Path.of(System.getProperty("test.classes"), "SignaturesTest$Observer.class"));
        var sig = observerCf.findAttribute(Attributes.signature()).orElseThrow().asClassSignature();
        var arrayListSig = sig.superclassSignature(); // ArrayList
        var arrayListTypeArg = (TypeArg.Bounded) arrayListSig.typeArgs().getFirst(); // Outer<String>.Inner<Long>
        assertEquals(TypeArg.Bounded.WildcardIndicator.NONE, arrayListTypeArg.wildcardIndicator());
        var innerSig = (ClassTypeSig) arrayListTypeArg.boundType();
        assertEquals("Inner", innerSig.className(), "simple name in signature");
        assertEquals(Outer.Inner.class.describeConstable().orElseThrow(), innerSig.classDesc(),
                "ClassDesc derived from signature");
    }

    static Stream<String> badTypeSignatures() {
        return """
                LObject
                LObject;B
                LIterable<LFoo>
                LIterable<<
                TBar
                TBar<LFoo;>
                B<LFoo;>
                B<LFoo;>;V
                X
                [LObject
                [LIterable<LFoo>
                [LIterable<<
                [TBar
                [TBar<LFoo;>
                [B<LFoo;>
                [X
                LSet<+Kind<**>;>;
                LSet<?Kind<*>;>;
                ()V
                Ljava/util/Opt<Ljava/lang/Integer;>ional;
                Lcom/example/Outer<Ljava/lang/String;>.package/Inner<[I>;
                LSample>;
                LSample:Other;
                LOuter<[JTT;>.[Inner;
                TA:J;
                LEmpty<>;
                L
                Lcom
                Lcom/example/
                Lcom/example/Outer<
                Lcom/example/Outer<Ljava/
                Lcom/example/Outer<Ljava/lang/String
                Lcom/example/Outer<Ljava/lang/String;
                Lcom/example/Outer<Ljava/lang/String;>
                Lcom/example/Outer<Ljava/lang/String;>.
                Lcom/example/Outer<Ljava/lang/String;>.Inner<[I>
                [V
                """.lines();
    }

    @ParameterizedTest
    @MethodSource("badTypeSignatures")
    void testBadTypeSignatures(String s) {
        assertThrows(IllegalArgumentException.class, () -> Signature.parseFrom(s));
    }

    static Stream<String> goodTypeSignatures() {
        return """
                Ljava/util/Optional<Ljava/lang/Integer;>;
                Lcom/example/Outer<Ljava/lang/Integer;>.Inner<[I>;
                LSample;
                LOuter<[JTT;>.Inner;
                LOuter.Inner;
                """.lines();
    }

    @ParameterizedTest
    @MethodSource("goodTypeSignatures")
    void testGoodTypeSignature(String s) {
        Signature.parseFrom(s);
    }

    static Stream<String> badClassSignatures() {
        return """
                Ljava/lang/Object;Ljava/lang/Iterable<LFoo;>
                LObject
                LObject;B
                LIterable<LFoo>
                LIterable<<
                TBar
                TBar<LFoo;>
                B<LFoo;>
                B<LFoo;>;V
                X
                LFoo<TK;>.It;L
                <K+LObject;>LFoo<TK;;>;LFoo<TK;>;LBar;
                <K:LObject;>>LFoo<TK;>;
                <K:LObject;>LFoo<+>;
                ()V
                <K:Ljava/lang/Object;>Ljava/lang/Object;TK;
                Ljava/lang/Object;[Ljava/lang/Object;
                [Ljava/util/Optional<[I>;
                [I
                <K:Ljava/lang/Object;>TK;
                <K;Q:Ljava/lang/Object;>Ljava/lang/Object;
                <:Ljava/lang/Object;>Ljava/lang/Object;
                <>Ljava/lang/Object;
                """.lines();
    }

    @ParameterizedTest
    @MethodSource("badClassSignatures")
    void testBadClassSignature(String s) {
        assertThrows(IllegalArgumentException.class, () -> ClassSignature.parseFrom(s));
    }

    static Stream<String> badMethodSignatures() {
        return """
                LObject;
                B
                ()V^
                ()V^B
                ()V^X
                (LObject;)
                (LObject)V
                ()LIterable
                ()LIterable
                ()TBar
                ()TBar;B
                (TBar<LFoo;
                (B<LFoo;>)V
                (X)
                ()X
                ()VB
                ()LSet<+Kin
                (LSet<?Kind
                <T::LA>()V
                (TT;I)VI
                (V)V
                """.lines();
    }

    @ParameterizedTest
    @MethodSource("badMethodSignatures")
    void testBadMethodSignature(String s) {
        assertThrows(IllegalArgumentException.class, () -> MethodSignature.parseFrom(s));
    }

    @Test
    void testArraySignatureLimits() {
        var sig = Signature.parseFrom("I");
        var arrSig = Signature.parseFrom("[I");
        for (int dim : List.of(256, -1, 0))
            assertThrows(IllegalArgumentException.class, () -> Signature.ArrayTypeSig.of(dim, sig));
        for (int dim : List.of(255, -1, 0))
            assertThrows(IllegalArgumentException.class, () -> Signature.ArrayTypeSig.of(dim, arrSig));
        for (int dim : List.of(255, 1))
            Signature.ArrayTypeSig.of(dim, sig);
        for (int dim : List.of(254, 1))
            Signature.ArrayTypeSig.of(dim, arrSig);
    }

    static Stream<Signature> longTypeSignatures() {
        var longAsciiName = "A" + "a".repeat(65536);
        var longCharName = "§".repeat(32768);
        var simpleClassSig = ClassTypeSig.of(longAsciiName);
        var nestedSig = ClassTypeSig.of(simpleClassSig, longCharName);
        var typeVarSig = TypeVarSig.of(longCharName);
        var parameterizedSig = ClassTypeSig.of(longCharName, TypeArg.of(nestedSig), TypeArg.unbounded());
        var parameterizedNestedSig = ClassTypeSig.of(nestedSig, longAsciiName, TypeArg.superOf(simpleClassSig));
        return Stream.of(simpleClassSig, nestedSig, typeVarSig, parameterizedSig, parameterizedNestedSig);
    }

    @ParameterizedTest
    @MethodSource("longTypeSignatures")
    void testLongTypeSignature(Signature sig) {
        var st = sig.signatureString();
        Signature.parseFrom(st); // Valid signature
        assertThrows(IllegalArgumentException.class, () -> SignatureAttribute.of(sig)); // Cannot write to class
    }

    static Stream<ClassSignature> longClassSignatures() {
        var longAsciiName = "A" + "a".repeat(65536);
        var longCharName = "§".repeat(32768);
        var simpleClassSig = ClassTypeSig.of(longAsciiName);
        var longSuperClass = ClassSignature.of(simpleClassSig);
        var longNameParam = TypeParam.of(longCharName, ClassTypeSig.of(CD_String));
        var longBoundParam = TypeParam.of("T", simpleClassSig);
        var longNameParamClass = ClassSignature.of(List.of(longNameParam), ClassTypeSig.of(CD_Object));
        var longBoundParamClass = ClassSignature.of(List.of(longBoundParam), ClassTypeSig.of(CD_Number));
        return Stream.of(longSuperClass, longNameParamClass, longBoundParamClass);
    }

    @ParameterizedTest
    @MethodSource("longClassSignatures")
    void testLongClassSignature(ClassSignature sig) {
        var st = sig.signatureString();
        ClassSignature.parseFrom(st); // Valid signature
        assertThrows(IllegalArgumentException.class, () -> SignatureAttribute.of(sig)); // Cannot write to class
    }

    static Stream<MethodSignature> longMethodSignatures() {
        var longAsciiName = "A" + "a".repeat(65536);
        var longCharName = "§".repeat(32768);
        var simpleClassSig = ClassTypeSig.of(longAsciiName);
        var longNameTypeVar = TypeVarSig.of(longCharName);
        var longReturnMethod = MethodSignature.of(simpleClassSig);
        var longNameParam = TypeParam.of(longCharName, ClassTypeSig.of(CD_String));
        var longNameParamMethod = MethodSignature.of(List.of(longNameParam), List.of(), BaseTypeSig.of(CD_void));
        var longThrowMethod = MethodSignature.of(List.of(), List.of(longNameTypeVar), ClassTypeSig.of(CD_Number));
        var longParameterMethod = MethodSignature.of(BaseTypeSig.of(CD_int), simpleClassSig);

        var eachParameter = ClassTypeSig.of("A" + "a".repeat(250));
        var parameterArray = Collections.nCopies(300, eachParameter).toArray(Signature[]::new);
        var manyParameterMethod = MethodSignature.of(BaseTypeSig.of(CD_void), parameterArray);
        return Stream.of(longReturnMethod, longNameParamMethod, longThrowMethod, longParameterMethod, manyParameterMethod);
    }

    @ParameterizedTest
    @MethodSource("longMethodSignatures")
    void testLongMethodSignature(MethodSignature sig) {
        var st = sig.signatureString();
        MethodSignature.parseFrom(st); // Valid signature
        assertThrows(IllegalArgumentException.class, () -> SignatureAttribute.of(sig)); // Cannot write to class
    }
}
